if (typeof(streamwork) == "undefined") {
    streamwork = {};
}
streamwork.odata = (function () {

    /**
     * Walks recursively through xml nodes. Calls options.before on each node when starts dealing with it. Stores returned result as beforeResult
     * Calls options.process on the node. If returned true, walks through each child recursively. Calls options.after with node and beforeResult
     */
    function walkRecursively(node, options){
        var beforeResult = options.before ? options.before(node) : null;
        if (options.process(node)){
            var count = node.childNodes.length;
            for(var i=0;i<count;i++){
                var subNode = node.childNodes[i];
                walkRecursively(subNode, options);
            }
        }
        if (options.after){
            options.after(node, beforeResult);
        }
    }

    /**
     * Reads xmlns="..." and xmlns:prefix="..." attributes from node and populates namespaces with {key - prefix, value - ns url}
     * Before updating namespaces retrieves old value for prefix. Returns hash of old namespace urls, with key - prefix, value - ns url
     */
    function collectNamespaces(node, namespaces){
        var oldNamespaces = {};
        var attrs = node.attributes;
        if (attrs){
            var count = attrs.length;
            for(var i=0;i<count;i++){
                var attr = attrs[i];
                var nameParts = attr.name.split(":");
                if (nameParts[0] == "xmlns"){
                    var nsPrefix = nameParts[1] || "";
                    oldNamespaces[nsPrefix] = namespaces[nsPrefix];
                    namespaces[nsPrefix] = attr.value;
                }
            }
        }
        return oldNamespaces;
    }

    /**
     * Returns hash of namespace prefix => namespace url defined in parent nodes
     */
    function parentNamespaces(node){
        var namespaces = {};
        // first element - immediate parent, last element - top node in document
        var parentNodes = [];
        var pnode = node.parentNode;
        while(pnode != null){
            parentNodes.push(pnode);
            pnode = pnode.parentNode;
        }
        // walk down from top node to immediate parent
        for(var i=parentNodes.length - 1;i>=0;i--){
            var n = parentNodes[i];
            collectNamespaces(n, namespaces);
        }
        return namespaces;
    }

    /**
     * Parses name that may or may not have prefix
     * parsePrefixedName("xxx") = {prefix:"", name:"xxx"}
     * parsePrefixedName("aaa:bbb") = {prefix:"aaa", name:"bbb"}
     */
    function parsePrefixedName(name){
        var nameParts = name.split(":");
        if (nameParts.length == 1){
            return {prefix: "", name: nameParts[0]};
        } else {
            return {prefix: nameParts[0], name: nameParts[1]};
        }
    }

    /**
     * Retrieves array of nodes starting with specified node, that belong to specified namespace and haev specified tag name. nsUrl = "*" means any namespace is ok
     * Recursively walks through subnodes maintaining hash of namespaces and collecting appropriate elements
     */
    function getElementsByTagNameNSImpl(startNode, nsUrl, tagName){
        var options = {
            // array of found nodes, populated during walking
            nodes: [],
            // key - ns prefix, i.e. "atom" or empty string for default; value - ns url.
            // after .after method is called, contains namespaces relevant for current node
            namespaces: parentNamespaces(startNode),
            before: function(node){
                return collectNamespaces(node, this.namespaces);
            },
            after:function(node, oldNamespaces){
                for(var nsPrefix in oldNamespaces) if (oldNamespaces.hasOwnProperty(nsPrefix)){
                    var value = oldNamespaces[nsPrefix];
                    if (value == undefined){
                        delete this.namespaces[nsPrefix];
                    } else {
                        this.namespaces[nsPrefix] = oldNamespaces[nsPrefix];
                    }
                }
            },
            process:function(node){
                if (node.tagName){
                    var parsed = parsePrefixedName(node.tagName);
                    if (parsed.name == tagName && (nsUrl == "*" || this.namespaces[parsed.prefix] == nsUrl)){
                        this.nodes.push(node);
                    }
                }
                return true;
            }
        };
        walkRecursively(startNode, options);
        return options.nodes;
    }

    /**
     * Walks through attributes of current specified node. If Attribute with specified name found, tries to determine it's
     * namespace url. It check current node for xmlns:prefix (or xmlns for default) namespace declarations, and if found checks if the url is equal to specified.
     * If urls match, returns attrbute's value, if they don't match - goes to the next attribute, and if namespace declaration not found in current node - goes to the parent one,
     * until no parent node found.
     */
    function getAttributeNSImpl(node, nsUrl, attrName){
        var attrs = node.attributes;
        if (attrs){
            var count = attrs.length;
            for(var i=0;i<count;i++){
                var attr = attrs[i];
                var parsed = parsePrefixedName(attr.name);
                if (parsed.name == attrName){ // needed attr name, now check namespace
                    if (nsUrl == "*"){
                        return attr.value;
                    }
                    var currNode = node;
                    while(currNode != null){
                        var namespaces = {};
                        collectNamespaces(currNode, namespaces);
                        var url = namespaces[parsed.prefix];
                        if (url != undefined){
                            if (url == nsUrl){
                                return attr.value;
                            } else {
                                break;
                            }
                        }
                        currNode = currNode.parentNode;
                    }
                }
            }
        }
        return null;
    }

    /**
     * Calls native node.getElementsByTagNameNS if implemented, otherwise calls getElementsByTagNameNSImpl
     */
    function getElementsByTagNameNS(startNode, nsUrl, tagName){
        if (typeof(startNode.getElementsByTagNameNS) == "function"){
            return startNode.getElementsByTagNameNS(nsUrl, tagName);
        } else {
            return getElementsByTagNameNSImpl(startNode, nsUrl, tagName);
        }
    }

    /**
     * Calls native .getAttributeNS if implemented, otherwise calls getAttributeNSImpl
     */
    function getAttributeNS(node, nsUrl, attrName){
        if (typeof(node.getAttributeNS) == "function"){
            return node.getAttributeNS(nsUrl, attrName);
        } else {
            return getAttributeNSImpl(node, nsUrl, attrName);
        }
    }

    // constructs hash where keys are elements of array and values are value
    // used to make it more straightforward to express arrays of string used later as set
    function arrayToHash(arr,value){
        var hash = {};
        for(var i=0;i<arr.length;i++){
            hash[arr[i]] = value;
        }
        return hash;
    }
    // hash of OData types that can be converted to native JS types
    // key - type, value - true
    var NATIVE_TYPES = arrayToHash(["Edm.Boolean", "Edm.Byte", "Edm.Int16", "Edm.Int32", "Edm.Int64", "Edm.SByte"], true);

    /**
     *
     * @param str string value of some field, taken from xml
     * @param type - OData type
     * @return if type corresponds to some JS native type, like Edm.Int32 corresponds to integer, this function
     *         returns native JS value. i.e. constructValue("1", "Edm.Int32") returns 1
     */
    function constructValue(str,type){
        if (NATIVE_TYPES[type]){
            // data comes from on-premise source, eval should be ok
            //TODO: use parseInt
            return eval(str);
        } else {
            return str;
        }
    }

    function populateJsonFromXml(json, xml, options){
        for(var j=0;j<xml.childNodes.length;j++){
            var fieldNode = xml.childNodes[j];
            var fieldName = parsePrefixedName(fieldNode.tagName).name;
            var value;
            var isNull = getAttrNSPatched(fieldNode, DATASERV_META_NS_URL, "Null") == "true";
            if (isNull){
                value = null;
            } else if (fieldNode.childNodes.length == 0 || (fieldNode.childNodes.length == 1 && typeof(fieldNode.childNodes[0].nodeValue) == "string")){
                var type = getAttrNSPatched(fieldNode, DATASERV_META_NS_URL, "Type"); // ODATA returned by SAP service has m:Type while normally it should be m:type
                value = constructValue(fieldNode.childNodes[0] ? fieldNode.childNodes[0].nodeValue : "", type);
            } else {
                //TODO: if tag is empty <d:name/> I don't know if it's empty string or empty object. I need metadata for that
                value = {};
                populateJsonFromXml(value, fieldNode, options);
            }
            if (options.lowerKeys){
                fieldName = fieldName.toLowerCase();
            }
            json[fieldName] = value;
        }
    }

    function resultFromEntry(entryNode,options) {
        var result = {};
        var idNode = getElementsByTagNameNS(entryNode, ATOM_NS_URL, "id")[0];
        var uri = idNode.childNodes[0].nodeValue;
        result.__metadata = {uri:uri};
        var catNode = getElementsByTagNameNS(entryNode, ATOM_NS_URL, "category")[0];
        if (catNode != null){
            result.__metadata.type = catNode.getAttribute("term");
        }
        var propNode = getElementsByTagNameNS(entryNode, DATASERV_META_NS_URL, "properties")[0];
        populateJsonFromXml(result, propNode, options);
        return result;
    }

    var ATOM_NS_URL = "http://www.w3.org/2005/Atom";
    var DATASERV_META_NS_URL = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata";
    var APP_NS_URL = "http://www.w3.org/2007/app";

    /**
     * Looks like SAP OData service has bug in it and returns OData with m:Type attributes, while it should be m:type
     * To make my code work for both correct and incorrect situation, I check both names here.
     */
    function getAttrNSPatched(node, nsUrl, attrName){
        // This is not very generic, because of a problem with attr="" because ("" || null) is null, while "" should be returned,
        // but currently it is used for only one attribute that is either non-empty string or not present
        return  getAttributeNS(node, nsUrl, attrName) ||
                getAttributeNS(node, nsUrl, attrName.toLowerCase()) ||
                node.getAttribute(attrName) ||
                node.getAttribute(attrName.toLowerCase());
    }

    function escapeAttr(value){
        return (""+value).replace(/&/g, "&amp;").replace(/"/g,"&quot;");
    }

    function escapeTag(value){
        return (""+value).replace(/&/g, "&amp;").replace(/</g,"&lt;");
    }
    /**
     * @param xml array fo strings
     * @param entry - hash keys/values
     */
    function appendEntry(xml, entry){
        xml.push('<atom:content type="application/xml"><m:properties>');
        for(var key in entry) if (entry.hasOwnProperty(key)){
            var value = entry[key];
            xml.push("<d:");
            xml.push(key);
            if (value === null || value === ""){
                xml.push(' m:null="true"/>');
            } else {
                xml.push(">");
                xml.push(escapeTag(value));
                xml.push("</d:");
                xml.push(key);
                xml.push(">")
            }
        }
        xml.push('</m:properties></atom:content><atom:title type="text">0</atom:title>');
    }

    /**
     * @param xml array fo strings
     * @param title title of link
     * @param link {rel: "some url", entries: [array of entries]}
     */
    function appendLink(xml, title, link){
        xml.push('<atom:link rel="');
        xml.push(escapeAttr(link.rel));
        xml.push('" type="application/atom+xml;type=feed" title="');
        xml.push(escapeAttr(title));
        xml.push('"><m:inline><atom:feed>');
        if (link.entries){
            for(var i=0;i<link.entries.length;i++){
                xml.push('<atom:entry>');
                appendEntry(xml, link.entries[i]);
                xml.push('</atom:entry>');
            }
        }
        xml.push('</atom:feed></m:inline></atom:link>');
    }

    var TOKEN_HEADER_NAME = "gateway-x-csrf-token";
    var COOKIE_HEADER_NAME = "gateway-cookie";
    var xCsrfToken = "Fetch";
    var cookie = null;

    return {
        /** Function for constructing OData XML based on javascript object. Used for constructing create/update OData messages
        returns string containing OData XML
        If this function is not enough for your OData constructing needs, construct OData XML on your own, or suggest enhancement to this function

        Obj should have following format
         {
            entry:{<key/value pairs of entry>},
            links: {LinkTitle: {res: "link url", entries:[{<key/value pairs of 1st entry}, {<key/value pairs of 2nd entry}, ...]}}
         }
         */
        odataJsonToXml: function(obj){
                var xml = ['<?xml version="1.0" encoding="utf-8"?>',
                            '<atom:entry xmlns:atom="http://www.w3.org/2005/Atom" ',
                            'xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" ',
                            'xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">'];
                appendEntry(xml, obj.entry);
                if (obj.links){
                    for(var linkTitle in obj.links) if (obj.links.hasOwnProperty(linkTitle)){
                        appendLink(xml, linkTitle, obj.links[linkTitle]);
                    }
                }
                xml.push('</atom:entry>');
                return xml.join('');
            },

        // Takes OData xml node and options for transforming into JSON object
        // returns javascript object that roughly corresponds to OData JSON representing same data.
        // By "roughly" I mean in those cases where I tested it, it looks similar to OData JSON, but I don't guarantee it's exactly the same.
        // options - may contain one or more of the following
        //      lowerKeys - means keys in json will be lower case even if in the XML they are not
        odataJsonFromXml: function(xml,options) {
                options = options || {};
                var json = null;
                var topTag = parsePrefixedName(xml.lastChild.tagName).name;
                var i;
                if (topTag == "service") { // list of collections
                    json = {d:{EntitySets:[]}};
                    var collectionNodes = getElementsByTagNameNS(xml, APP_NS_URL, "collection");
                    for (i = 0; i < collectionNodes.length; i++) {
                        var colNode = collectionNodes[i];
                        var colName = colNode.getAttribute("href");
                        json.d.EntitySets.push(colName);
                    }
                } else if (topTag == "feed") {  // collection of objects
                    json = {d:{results:[]}};
                    var entryNodes = getElementsByTagNameNS(xml, ATOM_NS_URL, "entry");
                    for (i = 0; i < entryNodes.length; i++) {
                        var entryNode = entryNodes[i];
                        json.d.results.push(resultFromEntry(entryNode,options));
                    }
                } else if (topTag == "entry") { // single object
                    json = {d:resultFromEntry(xml,options)};
                }
                // apparently $metadata can not be json
                return json;
            },

        /**
         * Performs OData request, calls callback with response.
         * @param service name of service
         * @param url url within service
         * @param settings settings passed to $.ajax calls. Do not put any functions here, only values. Like {type: "POST"}
         * @param callback callback to be called when response received
         */
        makeOrgRequest:function (service, url, settings, callback) {

            settings.headers = settings.headers || {};
            settings.headers[TOKEN_HEADER_NAME] = xCsrfToken;
            if (cookie){
                settings.headers[COOKIE_HEADER_NAME] = cookie;
            }


            function orgCallback(res) {
                if (res && res.success) {
                    if (res.responseText){
                        var contentType = res.headers["Content-Type"];
                        var scpos = contentType.indexOf(";");
                        if (scpos != -1) {
                            contentType = contentType.substring(0, scpos);
                        }
                        var text = res.responseText;
                        if (contentType.indexOf("xml") != -1) {
                            var xml = null;
                            if (window.DOMParser) {
                                var parser = new DOMParser();
                                xml = parser.parseFromString(text, "text/xml");
                            }
                            else { // Internet Explorer
                                xml = new ActiveXObject("Microsoft.XMLDOM");
                                xml.async = false;
                                xml.loadXML(text);
                            }
                            res.xml = xml;
                        } else if (contentType.indexOf("json") != -1) {
                            //TODO: use JSON.parse
                            res.json = eval("(" + text + ")");
                        }
                    }
                    if (res.headers){
                        if (res.headers[TOKEN_HEADER_NAME]){
                            xCsrfToken = res.headers[TOKEN_HEADER_NAME];
                        }
                        if (res.headers[COOKIE_HEADER_NAME]){
                            cookie = res.headers[COOKIE_HEADER_NAME];
                        }
                    }

                }
                callback(res);
            }

            gadgets.rpc.call('', 'streamwork.odata', orgCallback, { service:service,
                                                                    url:url,
                                                                    settings:settings});
        }
    }
})();
